りおんクロニクル


SQLite × MVVMアーキテクチャ最適化|業務アプリを長期運用できる構成にする実務ガイド【2026年版】

Home【2026年版】C# / .NET入門と実践ガイド|基礎・業務アプリ開発・SQLite連携まで体系的に解説

WPFやWinUIで業務アプリを作るとき、 SQLite × MVVM は「軽量・高速・保守しやすい」鉄板構成です。 しかし、設計を誤るとViewModelがDBコードだらけになり、 数年後には誰も触れないアプリになってしまいます。

この記事では、SQLiteとMVVMを組み合わせたときの 責務分離・レイヤー構成・非同期・テスト容易性を重視した “長期運用前提”のアーキテクチャを解説します。

この記事でわかること
・SQLite × MVVM の理想的なレイヤー構成
・Repository / Unit of Work / サービス層の役割
・ViewModelからDB依存を消す方法
・非同期処理(async/await)とUIフリーズ対策
・バリデーション・エラーハンドリングの置き場所
・テストしやすいMVVM構成

1. SQLite × MVVMの理想構成

まずは全体像から整理します。

■ レイヤー構成(論理)

  1. View(WPF XAML)
  2. ViewModel(画面ロジック・状態管理)
  3. サービス層(ユースケース・業務ロジック)
  4. Repository / Unit of Work(DBアクセス抽象化)
  5. SQLite(Dapper / EF Core)

ポイントは、ViewModelがSQLiteを知らないこと。 ViewModelは「サービス層のメソッドを呼ぶだけ」にしておくと、 テスト・差し替え・将来のDB移行が圧倒的に楽になります。

2. Repository / Unit of Work の役割

SQLiteへの生SQLやDapper/EF Coreのコードは、 RepositoryとUnit of Workに閉じ込めます。

■ IUserRepository(例)

public interface IUserRepository
{
    Task<IEnumerable<User>> GetAllAsync();
    Task<User?> GetByIdAsync(int id);
    Task AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

■ IUnitOfWork

public interface IUnitOfWork : IAsyncDisposable
{
    IUserRepository Users { get; }
    Task CommitAsync();
    Task RollbackAsync();
}

ViewModelはUnit of Workの存在すら知らず、 サービス層がUoWを使ってトランザクションをまとめる構造にします。

3. サービス層で「ユースケース」を表現する

サービス層は、画面から見た「やりたいこと」をそのままメソッドにします。

■ IUserService(例)

public interface IUserService
{
    Task<IEnumerable<UserDto>> GetUsersAsync();
    Task SaveUserAsync(UserDto dto);
    Task DeleteUserAsync(int id);
}

■ 実装例

public class UserService : IUserService
{
    private readonly IUnitOfWorkFactory _uowFactory;

    public UserService(IUnitOfWorkFactory uowFactory)
        => _uowFactory = uowFactory;

    public async Task<IEnumerable<UserDto>> GetUsersAsync()
    {
        await using var uow = _uowFactory.Create();
        var users = await uow.Users.GetAllAsync();
        return users.Select(u => new UserDto
        {
            Id = u.Id,
            Name = u.Name,
            Age = u.Age
        });
    }

    public async Task SaveUserAsync(UserDto dto)
    {
        await using var uow = _uowFactory.Create();

        if (dto.Id == 0)
        {
            await uow.Users.AddAsync(new User
            {
                Name = dto.Name,
                Age = dto.Age
            });
        }
        else
        {
            var user = await uow.Users.GetByIdAsync(dto.Id);
            if (user is null) return;

            user.Name = dto.Name;
            user.Age = dto.Age;
            await uow.Users.UpdateAsync(user);
        }

        await uow.CommitAsync();
    }
}

ViewModelはこのサービスを呼ぶだけで、 トランザクション・DB・マッピングを意識しなくて済みます。

4. ViewModelの責務を「画面ロジック」に限定する

ViewModelは、画面の状態管理とコマンド実行に集中させます。

■ UserListViewModel の例

public class UserListViewModel : INotifyPropertyChanged
{
    private readonly IUserService _service;

    public ObservableCollection<UserDto> Users { get; } = new();
    public ICommand LoadCommand { get; }
    public ICommand SaveCommand { get; }

    private UserDto? _selectedUser;
    public UserDto? SelectedUser
    {
        get => _selectedUser;
        set { _selectedUser = value; OnPropertyChanged(); }
    }

    public UserListViewModel(IUserService service)
    {
        _service = service;
        LoadCommand = new AsyncRelayCommand(LoadAsync);
        SaveCommand = new AsyncRelayCommand(SaveAsync);
    }

    private async Task LoadAsync()
    {
        var list = await _service.GetUsersAsync();
        Users.Clear();
        foreach (var u in list)
            Users.Add(u);
    }

    private async Task SaveAsync()
    {
        if (SelectedUser is null) return;
        await _service.SaveUserAsync(SelectedUser);
        await LoadAsync();
    }

    // INotifyPropertyChanged 実装は省略
}

ここにはSQLite・Dapper・SQL文が一切出てこないのが理想です。

5. 非同期処理とUIフリーズ対策

SQLiteアクセスは必ずasync/awaitで行い、 UIスレッドをブロックしないようにします。

■ 非同期コマンド(AsyncRelayCommand)

public class AsyncRelayCommand : ICommand
{
    private readonly Func<Task> _execute;
    private bool _isExecuting;

    public AsyncRelayCommand(Func<Task> execute)
        => _execute = execute;

    public bool CanExecute(object? parameter) => !_isExecuting;

    public async void Execute(object? parameter)
    {
        if (_isExecuting) return;
        _isExecuting = true;
        try { await _execute(); }
        finally { _isExecuting = false; }
    }

    public event EventHandler? CanExecuteChanged;
}

これにより、DBが重くてもUIは固まらない構成になります。

6. バリデーションとエラーハンドリングの置き場所

バリデーションはViewModelとサービス層の両方に分けて考えます。

■ ViewModel側

■ サービス層側

例外(SQLiteExceptionなど)はサービス層で捕捉し、 ViewModelには「ユーザー向けメッセージ」として返すと綺麗に分離できます。

7. テスト容易性を高めるポイント

この構成にしておくと、次のようなテストが簡単になります。

特に、ViewModelがDBを知らないことが テスト容易性に直結します。

8. 業務アプリ向けベストプラクティス

まとめ:SQLite × MVVMは“責務分離”がすべて

「とりあえず動く」MVVMから、「10年保守できる」MVVMへ。 この記事の構成をベースに、あなたのSQLite × MVVMアプリを 一段上のアーキテクチャに仕上げてみてください。

前のページ  次のページ